\section{异常}
程序的错误大致可以分为三种，分别是语法错误、逻辑错误和运行时错误：
\begin{enumerate}
    \item 语法错误在编译和链接阶段就能发现，只有 100\% 符合语法规则的代码才能生成可执行程序。语法错误是最容易发现、最容易定位、最容易排除的错误，程序员最不需要担心的就是这种错误。
    \item 逻辑错误是说我们编写的代码思路有问题，不能够达到最终的目标，这种错误可以通过调试来解决。
    \item 运行时错误是指程序在运行期间发生的错误，例如除数为 0、内存分配失败、数组越界、文件不存在等。C++ 异常（Exception）机制就是为解决运行时错误而引入的。
\end{enumerate}
运行时错误如果放任不管，系统就会执行默认的操作，终止程序运行，也就是我们常说的程序崩溃（Crash）。C++ 提供了异常（Exception）机制，让我们能够捕获运行时错误，给程序一次“起死回生”的机会，或者至少告诉用户发生了什么再终止程序。

一个发生运行时错误的程序：
\cppfile{code/exception/demo1.cpp}
运行代码，在控制台输出 ch1 的值后程序崩溃。下面我们来分析一下原因。

at() 是 string 类的一个成员函数，它会根据下标来返回字符串的一个字符。与[ ]不同，at() 会检查下标是否越界，如果越界就抛出一个异常；而[ ]不做检查，不管下标是多少都会照常访问。
所谓抛出异常，就是报告一个运行时错误，程序员可以根据错误信息来进一步处理。
上面的代码中，下标 100 显然超出了字符串 str 的长度。由于第 6 行代码不会检查下标越界，虽然有逻辑错误，但是程序能够正常运行。而第 8 行代码则不同，at() 函数检测到下标越界会抛出一个异常，这个异常可以由程序员处理，但是我们在代码中并没有处理，所以系统只能执行默认的操作，也即终止程序执行。

\subsection{捕获异常}
我们可以借助 C++ 异常机制来捕获上面的异常，避免程序崩溃。捕获异常的语法为：
\begin{cpp}
    try{
    // 可能抛出异常的语句
    }catch(exceptionType variable){
            // 处理异常的语句
        }
\end{cpp}

try和catch都是 C++ 中的关键字，后跟语句块，不能省略{ }。try 中包含可能会抛出异常的语句，一旦有异常抛出就会被后面的 catch 捕获。从 try 的意思可以看出，它只是“检测”语句块有没有异常，如果没有发生异常，它就“检测”不到。catch 是“抓住”的意思，用来捕获并处理 try 检测到的异常；如果 try 语句块没有检测到异常（没有异常抛出），那么就不会执行 catch 中的语句。

这就好比，catch 告诉 try：你去检测一下程序有没有错误，有错误的话就告诉我，我来处理，没有的话就不要理我！

catch 关键字后面的exceptionType variable指明了当前 catch 可以处理的异常类型，以及具体的出错信息。我们稍后再对异常类型展开讲解，当务之急是演示一下 try-catch 的用法，先让读者有一个整体上的认识。

\textbf{修改上面的代码，加入捕获异常的语句}
\cppfile{code/exception/demo2.cpp}
运行结果：
\begin{cpp}
    访问异常
\end{cpp}
可以看出，第一个 try 没有捕获到异常，输出了一个没有意义的字符（垃圾值）。因为[ ]不会检查下标越界，不会抛出异常，所以即使有错误，try 也检测不到。换句话说，发生异常时必须将异常明确地抛出，try 才能检测到；如果不抛出来，即使有异常 try 也检测不到。所谓抛出异常，就是明确地告诉程序发生了什么错误。

第二个 try 检测到了异常，并交给 catch 处理，执行 catch 中的语句。需要说明的是，异常一旦抛出，会立刻被 try 检测到，并且不会再执行异常点（异常发生位置）后面的语句。本例中抛出异常的位置是 at() 函数，它后面的 cout 语句就不会再被执行，所以看不到它的输出。

说得直接一点，检测到异常后程序的执行流会发生跳转，从异常点跳转到 catch 所在的位置，位于异常点之后的、并且在当前 try 块内的语句就都不会再执行了；即使 catch 语句成功地处理了错误，程序的执行流也不会再回退到异常点，所以这些语句永远都没有执行的机会了。本例中，第 18 行代码就是被跳过的代码。

执行完 catch 块所包含的代码后，程序会继续执行 catch 块后面的代码，就恢复了正常的执行流。

为了演示「不明确地抛出异常就检测不到异常」，大家不妨将第 10 行代码改为char ch1 = str[100000000];，访问第 100 个字符可能不会发生异常，但是访问第 1 亿个字符肯定会发生异常了，这个异常就是内存访问错误。运行更改后的程序，会发现第 10 行代码产生了异常，导致程序崩溃了，这说明 try-catch 并没有捕获到这个异常。

关于「如何抛出异常」，我们将在下节讲解，这里重点是让大家明白异常的处理流程：
抛出（Throw）--> 检测（Try） --> 捕获（Catch）

发生异常的位置
异常可以发生在当前的 try 块中，也可以发生在 try 块所调用的某个函数中，或者是所调用的函数又调用了另外的一个函数，这个另外的函数中发生了异常。这些异常，都可以被 try 检测到。

下面的例子演示了 try 块中直接发生的异常：
\cppfile{code/exception/demo3.cpp}
运行结果：
\begin{bash}
    抛出异常
\end{bash}
throw关键字用来抛出一个异常，这个异常会被 try 检测到，进而被 catch 捕获。关于 throw 的用法，我们将在下节深入讲解，这里大家只需要知道，在 try 块中直接抛出的异常会被 try 检测到。

下面的例子演示了 try 块中调用的某个函数中发生了异常：
\cppfile{code/exception/demo4.cpp}
运行结果：
\begin{bash}
    调用函数
    捕获函数抛出的异常
\end{bash}
func() 在 try 块中被调用，它抛出的异常会被 try 检测到，进而被 catch 捕获。从运行结果可以看出，func() 中的 cout 和 try 中的 cout 都没有被执行。

try 块中调用了某个函数，该函数又调用了另外的一个函数，这个另外的函数抛出了异常：
\cppfile{code/exception/demo5.cpp}
运行结果：
\begin{bash}
    调用函数
    捕获函数抛出的异常
\end{bash}
发生异常后，程序的执行流会沿着函数的调用链往前回退，直到遇见 try 才停止。在这个回退过程中，调用链中剩下的代码（所有函数中未被执行的代码）都会被跳过，没有执行的机会了。

首先来回顾一下上节讲到的 try-catch 的用法：
\begin{cpp}
    try{
    // 可能抛出异常的语句
    }catch(exceptionType variable){
            // 处理异常的语句
        }
\end{cpp}

exceptionType是异常类型，它指明了当前的 catch 可以处理什么类型的异常；variable是一个变量，用来接收异常信息。当程序抛出异常时，会创建一份数据，这份数据包含了错误信息，程序员可以根据这些信息来判断到底出了什么问题，接下来怎么处理。

异常既然是一份数据，那么就应该有数据类型。C++ 规定，异常类型可以是 int、char、float、bool 等基本类型，也可以是指针、数组、字符串、结构体、类等聚合类型。C++ 语言本身以及标准库中的函数抛出的异常，都是 exception 类或其子类的异常。也就是说，抛出异常时，会创建一个 exception 类或其子类的对象。

exceptionType variable和函数的形参非常类似，当异常发生后，会将异常数据传递给 variable 这个变量，这和函数传参的过程类似。当然，只有跟 exceptionType 类型匹配的异常数据才会被传递给 variable，否则 catch 不会接收这份异常数据，也不会执行 catch 块中的语句。换句话说，catch 不会处理当前的异常。

我们可以将 catch 看做一个没有返回值的函数，当异常发生后 catch 会被调用，并且会接收实参（异常数据）。

但是 catch 和真正的函数调用又有区别：

真正的函数调用，形参和实参的类型必须要匹配，或者可以自动转换，否则在编译阶段就报错了。
而对于 catch，异常是在运行阶段产生的，它可以是任何类型，没法提前预测，所以不能在编译阶段判断类型是否正确，只能等到程序运行后，真的抛出异常了，再将异常类型和 catch 能处理的类型进行匹配，匹配成功的话就“调用”当前的 catch，否则就忽略当前的 catch。

总起来说，catch 和真正的函数调用相比，多了一个「在运行阶段将实参和形参匹配」的过程。

另外需要注意的是，如果不希望 catch 处理异常数据，也可以将 variable 省略掉，也即写作：
\begin{cpp}
    try{
    // 可能抛出异常的语句
    }catch(exceptionType){
            // 处理异常的语句
        }
\end{cpp}
这样只会将异常类型和 catch 所能处理的类型进行匹配，不会传递异常数据了。
多级 catch
前面的例子中，一个 try 对应一个 catch，这只是最简单的形式。其实，一个 try 后面可以跟多个 catch：
\begin{cpp}
    try{
    //可能抛出异常的语句
    }catch (exception_type_1 e){
            //处理异常的语句
        }catch (exception_type_2 e){
            //处理异常的语句
        }
    //其他的catch
    catch (exception_type_n e){
            //处理异常的语句
        }
\end{cpp}
当异常发生时，程序会按照从上到下的顺序，将异常类型和 catch 所能接收的类型逐个匹配。一旦找到类型匹配的 catch 就停止检索，并将异常交给当前的 catch 处理（其他的 catch 不会被执行）。如果最终也没有找到匹配的 catch，就只能交给系统处理，终止程序的运行。

下面的例子演示了多级 catch 的使用：
\begin{cpp}
    #include <iostream>
    #include <string>
    using namespace std;
    class Base{ };
    class Derived: public Base{ };
    int main(){
            try{
                    throw Derived();  //抛出自己的异常类型，实际上是创建一个Derived类型的匿名对象
                    cout<<"This statement will not be executed."<<endl;
                }catch(int){
                    cout<<"Exception type: int"<<endl;
                }catch(char *){
                    cout<<"Exception type: cahr *"<<endl;
                }catch(Base){  //匹配成功（向上转型）
                    cout<<"Exception type: Base"<<endl;
                }catch(Derived){
                    cout<<"Exception type: Derived"<<endl;
                }
            return 0;
        }
\end{cpp}
运行结果：
\begin{bash}
    Exception type: Base
\end{bash}
在 catch 中，我们只给出了异常类型，没有给出接收异常信息的变量。
本例中，我们定义了一个基类 Base，又从 Base 派生类出了 Derived。抛出异常时，我们创建了一个 Derived 类的匿名对象，也就是说，异常的类型是 Derived。

我们期望的是，异常被catch(Derived)捕获，但是从输出结果可以看出，异常提前被catch(Base)捕获了，这说明 catch 在匹配异常类型时发生了向上转型（Upcasting）。
catch 在匹配过程中的类型转换
C/C++ 中存在多种多样的类型转换，以普通函数（非模板函数）为例，发生函数调用时，如果实参和形参的类型不是严格匹配，那么会将实参的类型进行适当的转换，以适应形参的类型，这些转换包括：
算数转换：例如 int 转换为 float，char 转换为 int，double 转换为 int 等。
向上转型：也就是派生类向基类的转换，请猛击《C++向上转型（将派生类赋值给基类）》了解详情。
const 转换：也即将非 const 类型转换为 const 类型，例如将 char * 转换为 const char *。
数组或函数指针转换：如果函数形参不是引用类型，那么数组名会转换为数组指针，函数名也会转换为函数指针。
用户自定的类型转换。

catch 在匹配异常类型的过程中，也会进行类型转换，但是这种转换受到了更多的限制，仅能进行「向上转型」、「const 转换」和「数组或函数指针转换」，其他的都不能应用于 catch。

向上转型在上面的例子中已经发生了，下面的例子演示了 const 转换以及数组和指针的转换：
\begin{cpp}
    #include <iostream>
    using namespace std;
    int main(){
            int nums[] = {1, 2, 3};
            try{
                    throw nums;
                    cout<<"This statement will not be executed."<<endl;
                }catch(const int *){
                    cout<<"Exception type: const int *"<<endl;
                }
            return 0;
        }
\end{cpp}
运行结果：
\begin{bash}
    Exception type: const int *
\end{bash}
nums 本来的类型是int [3]，但是 catch 中没有严格匹配的类型，所以先转换为int *，再转换为const int *。
数组也是一种类型，数组并不等价于指针，这点已在《数组和指针绝不等价，数组是另外一种类型》和《数组到底在什么时候会转换为指针》中进行了详细讲解。


在《C++异常处理》一节中，我们讲到了 C++ 异常处理的流程，具体为：
抛出（Throw）--> 检测（Try） --> 捕获（Catch）

异常必须显式地抛出，才能被检测和捕获到；如果没有显式的抛出，即使有异常也检测不到。

在 C++ 中，我们使用 throw 关键字来显式地抛出异常，它的用法为：
\cppinline{throw exceptionData;}

exceptionData 是“异常数据”的意思，它可以包含任意的信息，完全有程序员决定。exceptionData 可以是 int、float、bool 等基本类型，也可以是指针、数组、字符串、结构体、类等聚合类型，请看下面的例子：
\begin{cpp}
    char str[] = "http://c.biancheng.net";
    char *pstr = str;
    class Base{};
    Base obj;
    throw 100;  //int 类型
    throw str;  //数组类型
    throw pstr;  //指针类型
    throw obj;  //对象类型
\end{cpp}
一个动态数组的例子
C/C++ 规定，数组一旦定义后，它的长度就不能改变了；换句话说，数组容量不能动态地增大或者减小。这样的数组称为静态数组（Static array）。静态数组有时候会给编码代码不便，我们可以通过自定义的 Array 类来实现动态数组（Dynamic array）。所谓动态数组，是指数组容量能够在使用的过程中随时增大或减小。

下面这段代码虽然有点长，但它是一个典型的使用异常的场景，请大家耐心阅读。
\begin{cpp}
    #include <iostream>
    #include <cstdlib>
    using namespace std;
    //自定义的异常类型
    class OutOfRange{
            public:
            OutOfRange(): m_flag(1){ };
            OutOfRange(int len, int index): m_len(len), m_index(index), m_flag(2){ }
            public:
            void what() const;  //获取具体的错误信息
            private:
            int m_flag;  //不同的flag表示不同的错误
            int m_len;  //当前数组的长度
            int m_index;  //当前使用的数组下标
        };
    void OutOfRange::what() const {
    if(m_flag == 1){
            cout<<"Error: empty array, no elements to pop."<<endl;
        }else if(m_flag == 2){
            cout<<"Error: out of range( array length "<<m_len<<", access index "<<m_index<<" )"<<endl;
        }else{
    cout<<"Unknown exception."<<endl;
    }
    }
    //实现动态数组
    class Array{
            public:
            Array();
            ~Array(){ free(m_p); };
            public:
            int operator[](int i) const;  //获取数组元素
            int push(int ele);  //在末尾插入数组元素
            int pop();  //在末尾删除数组元素
            int length() const{ return m_len; };  //获取数组长度
            private:
            int m_len;  //数组长度
            int m_capacity;  //当前的内存能容纳多少个元素
            int *m_p;  //内存指针
            private:
            static const int m_stepSize = 50;  //每次扩容的步长
        };
    Array::Array(){
            m_p = (int*)malloc( sizeof(int) * m_stepSize );
            m_capacity = m_stepSize;
            m_len = 0;
        }
    int Array::operator[](int index) const {
            if( index<0 || index>=m_len ){  //判断是否越界
                    throw OutOfRange(m_len, index);  //抛出异常（创建一个匿名对象）
                }
            return *(m_p + index);
        }
    int Array::push(int ele){
            if(m_len >= m_capacity){  //如果容量不足就扩容
                    m_capacity += m_stepSize;
                    m_p = (int*)realloc( m_p, sizeof(int) * m_capacity );  //扩容
                }
            *(m_p + m_len) = ele;
            m_len++;
            return m_len-1;
        }
    int Array::pop(){
            if(m_len == 0){
                    throw OutOfRange();  //抛出异常（创建一个匿名对象）
                }
            m_len--;
            return *(m_p + m_len);
        }
    //打印数组元素
    void printArray(Array &arr){
    int len = arr.length();
    //判断数组是否为空
    if(len == 0){
            cout<<"Empty array! No elements to print."<<endl;
            return;
        }
    for(int i=0; i<len; i++){
    if(i == len-1){
            cout<<arr[i]<<endl;
        }else{
    cout<<arr[i]<<", ";
    }
    }
    }
    int main(){
            Array nums;
            //向数组中添加十个元素
            for(int i=0; i<10; i++){
                    nums.push(i);
                }
            printArray(nums);
            //尝试访问第20个元素
            try{
                    cout<<nums[20]<<endl;
                }catch(OutOfRange &e){
                    e.what();
                }
            //尝试弹出20个元素
            try{
                    for(int i=0; i<20; i++){
                            nums.pop();
                        }
                }catch(OutOfRange &e){
                    e.what();
                }
            printArray(nums);
            return 0;
        }
\end{cpp}
运行结果：
\begin{bash}
    0, 1, 2, 3, 4, 5, 6, 7, 8, 9
    Error: out of range( array length 10, access index 20 )
    Error: empty array, no elements to pop.
    Empty array! No elements to print.
\end{bash}
Array 类实现了动态数组，它的主要思路是：在创建对象时预先分配出一定长度的内存（通过 malloc() 分配），内存不够用时就再扩展内存（通过 realloc() 重新分配）。Array 数组只能在尾部一个一个地插入（通过 push() 插入）或删除（通过 pop() 删除）元素。

我们通过重载过的[ ]运算符来访问数组元素，如果下标过小或过大，就会抛出异常（第53行代码）；在抛出异常的同时，我们还记录了当前数组的长度和要访问的下标。

在使用 pop() 删除数组元素时，如果当前数组为空，也会抛出错误。
throw 用作异常规范
throw 关键字除了可以用在函数体中抛出异常，还可以用在函数头和函数体之间，指明当前函数能够抛出的异常类型，这称为异常规范（Exception specification），有些教程也称为异常指示符或异常列表。请看下面的例子：
double func (char param) throw (int);

这条语句声明了一个名为 func 的函数，它的返回值类型为 double，有一个 char 类型的参数，并且只能抛出 int 类型的异常。如果抛出其他类型的异常，try 将无法捕获，只能终止程序。

如果函数会抛出多种类型的异常，那么可以用逗号隔开：
double func (char param) throw (int, char, exception);

如果函数不会抛出任何异常，那么( )中什么也不写：
double func (char param) throw ();

如此，func() 函数就不能抛出任何类型的异常了，即使抛出了，try 也检测不到。
1) 虚函数中的异常规范
C++ 规定，派生类虚函数的异常规范必须与基类虚函数的异常规范一样严格，或者更严格。只有这样，当通过基类指针（或者引用）调用派生类虚函数时，才能保证不违背基类成员函数的异常规范。请看下面的例子：
\begin{cpp}
    class Base{
            public:
            virtual int fun1(int) throw();
            virtual int fun2(int) throw(int);
            virtual string fun3() throw(int, string);
        };
    class Derived:public Base{
            public:
            int fun1(int) throw(int);   //错！异常规范不如 throw() 严格
            int fun2(int) throw(int);   //对！有相同的异常规范
            string fun3() throw(string);  //对！异常规范比 throw(int,string) 更严格
        }
\end{cpp}
2) 异常规范与函数定义和函数声明
C++ 规定，异常规范在函数声明和函数定义中必须同时指明，并且要严格保持一致，不能更加严格或者更加宽松。

请看下面的几组函数：
\begin{cpp}
    //错！定义中有异常规范，声明中没有
    void func1();
    void func1() throw(int) { }
    //错！定义和声明中的异常规范不一致
    void func2() throw(int);
    void func2() throw(int, bool) { }
    //对！定义和声明中的异常规范严格一致
    void func3() throw(float, char*);
    void func3() throw(float, char*) { }
\end{cpp}
请抛弃异常规范，不要再使用它
异常规范的初衷是好的，它希望让程序员看到函数的定义或声明后，立马就知道该函数会抛出什么类型的异常，这样程序员就可以使用 try-catch 来捕获了。如果没有异常规范，程序员必须阅读函数源码才能知道函数会抛出什么异常。

不过这有时候也不容易做到。例如，func\_outer() 函数可能不会引发异常，但它调用了另外一个函数 func\_inner()，这个函数可能会引发异常。再如，您编写的函数调用了老式的库函数，此时不会引发异常，但是库更新以后这个函数却引发了异常。总之，异常规范的初衷实现起来有点困难，所以大家达成的一致意见是，最好不要使用异常规范。

异常规范是 C++98 新增的一项功能，但是后来的 C++11 已经将它抛弃了，不再建议使用。

另外，各个编译器对异常规范的支持也不一样，请看下面的代码：
\begin{cpp}
    #include <iostream>
    #include <string>
    #include <exception>
    using namespace std;
    void func()throw(char*, exception){
    throw 100;
    cout<<"[1]This statement will not be executed."<<endl;
    }
    int main(){
            try{
                    func();
                }catch(int){
                    cout<<"Exception type: int"<<endl;
                }
            return 0;
        }
\end{cpp}
在 GCC 下，这段代码运行到第 7 行时程序会崩溃。虽然 func() 函数中发生了异常，但是由于 throw 限制了函数只能抛出 char*、exception 类型的异常，所以 try-catch 将捕获不到异常，只能交给系统处理，终止程序。

在 Visual C++ 下，输出结果为Exception type: int，这说明异常被成功捕获了。在 Visual C++ 中使用异常规范虽然没有语法错误，但是也没有任何效果，Visual C++ 会直接忽略异常规范的限制，函数可以抛出任何类型的异常。


C++语言本身或者标准库抛出的异常都是 exception 的子类，称为标准异常（Standard Exception）。你可以通过下面的语句来捕获所有的标准异常：
\begin{bash}
    try{
    //可能抛出异常的语句
    }catch(exception &e){
            //处理异常的语句
        }
\end{bash}
之所以使用引用，是为了提高效率。如果不使用引用，就要经历一次对象拷贝（要调用拷贝构造函数）的过程。

exception 类位于 <exception> 头文件中，它被声明为：
\begin{cpp}
    class exception{
            public:
            exception () throw();  //构造函数
            exception (const exception&) throw();  //拷贝构造函数
            exception& operator= (const exception&) throw();  //运算符重载
            virtual ~exception() throw();  //虚析构函数
            virtual const char* what() const throw();  //虚函数
        }
\end{cpp}
这里需要说明的是 what() 函数。what() 函数返回一个能识别异常的字符串，正如它的名字“what”一样，可以粗略地告诉你这是什么异常。不过C++标准并没有规定这个字符串的格式，各个编译器的实现也不同，所以 what() 的返回值仅供参考。

下\figref{fig:exception}展示了 exception 类的继承层次：
\begin{figure}[!htbp]
    \centering
    \includegraphics[width=\textwidth]{exception}
    \caption{exception异常}
    \label{fig:exception}
\end{figure}
先来看一下 exception 类的直接派生类：
\begin{table}[!htbp]
    \begin{tabular}{p{0.5\textwidth}p{0.5\textwidth}}
        \toprule
        异常名称           & 说  明                                                                                                                                                                                               \\
        \midrule
        logic\_error       & 逻辑错误。                                                                                                                                                                                           \\
        runtime\_error     & 运行时错误。                                                                                                                                                                                         \\
        bad\_alloc         & 使用 new 或 new[ ] 分配内存失败时抛出的异常。                                                                                                                                                        \\
        bad\_typeid        & 使用 typeid 操作一个 NULL 指针，而且该指针是带有虚函数的类，这时抛出 bad\_typeid 异常。                                                                                                              \\
        bad\_cast          & 使用 dynamic\_cast 转换失败时抛出的异常。                                                                                                                                                            \\
        ios\_base::failure & io 过程中出现的异常。                                                                                                                                                                                \\
        bad\_exception     & 这是个特殊的异常，如果函数的异常列表里声明了 bad\_exception 异常，当函数内部抛出了异常列表中没有的异常时，如果调用的 unexpected() 函数中抛出了异常，不论什么类型，都会被替换为 bad\_exception 类型。 \\
        \bottomrule
    \end{tabular}
\end{table}
logic\_error 的派生类：
\begin{table}[!htbp]
    \begin{tabular}{p{0.5\textwidth}p{0.5\textwidth}}
        \toprule
        异常名称          & 说  明                                                                                                    \\
        \midrule
        length\_error     & 试图生成一个超出该类型最大长度的对象时抛出该异常，例如 vector 的 resize 操作。                            \\
        domain\_error     & 参数的值域错误，主要用在数学函数中，例如使用一个负值调用只能操作非负数的函数。                            \\
        out\_of\_range    & 超出有效范围。                                                                                            \\
        invalid\_argument & 参数不合适。在标准库中，当利用string对象构造 bitset 时，而 string 中的字符不是 0 或1 的时候，抛出该异常。 \\
        \bottomrule
    \end{tabular}
\end{table}
runtime\_error 的派生类：
\begin{table}[!htbp]
    \begin{tabular}{p{0.5\textwidth}p{0.5\textwidth}}
        \toprule
        异常名称         & 说  明                           \\
        \midrule
        range\_error     & 计算结果超出了有意义的值域范围。 \\
        overflow\_error  & 算术计算上溢。                   \\
        underflow\_error & 算术计算下溢。                   \\
    \end{tabular}
\end{table}
